package top.v5it.japi.plus.extra.web;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import org.springframework.aop.support.AopUtils;
import org.springframework.web.bind.annotation.*;
import top.v5it.japi.plus.core.spring.web.JapiEada;
import top.v5it.japi.plus.extra.entity.Entity;
import top.v5it.japi.plus.extra.exception.JapiOrmException;
import top.v5it.japi.plus.extra.util.EntityUtils;
import top.v5it.japi.plus.log.JapiOperLog;
import top.v5it.japi.plus.support.limit.JapiLimit;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Objects;

/**
 * 统一ORM的CRUD操作生成控制器，可支持 POST 或者 GET 方法
 *
 * <pre>
 * ### 示例一
 * GET http://localhost:7474/orm/account/searchMapByCondition?name=jack&mapKey=accountNumber
 * Content-Type: application/x-www-form-urlencoded
 * Accept: application/json
 *
 * ### 示例二
 * GET http://localhost:7474/orm/account/searchMapByCondition
 * Content-Type: application/x-www-form-urlencoded
 * Accept: application/json
 *
 * name=jack&mapKey=accountNumber
 *
 * ### 示例三
 * GET http://localhost:7474/orm/account/searchMapByCondition?name=jack
 * Content-Type: application/x-www-form-urlencoded
 * Accept: application/json
 *
 * mapKey=accountNumber
 *
 * ### 示例四
 * GET http://localhost:7474/orm/account/searchMapByCondition
 * Content-Type: application/json
 * Accept: application/json
 *
 * {
 *   "mapKey": "accountNumber",
 *   "name": "jack"
 * }
 *
 * ### 示例五
 * GET http://localhost:7474/orm/account/searchMapByCondition?mapKey=accountNumber
 * Content-Type: application/json
 * Accept: application/json
 *
 * {
 *   "name": "jack"
 * }
 * </pre>
 *
 * @author zhanpu
 * @date 2022/8/10
 */
@RestController
public class JapiOrmController extends JapiBaseController {

    private static final String ORM_URL = "orm/{bean}/{method}";

    @JapiEada
    @JapiOperLog("ORM的CRUD操作")
    @JapiLimit(limitKey = ORM_URL)
    @RequestMapping(value = ORM_URL, method = {RequestMethod.GET, RequestMethod.POST})
    public Object execute(@PathVariable("bean") String bean, @PathVariable("method") String methodName,
                          @RequestBody(required = false) Map<String, Object> json, HttpServletRequest request) {
        Object service;
        try {
            service = getBean(bean);
        } catch (RuntimeException e) {
            throw new JapiOrmException(e);
        }
        Method method = getMethod(service, bean, methodName);
        json = toMap(json, request);
        final Class<?> targetClass = service.getClass();
        final Method mostSpecificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
        Object vo = getValueObj(method, json);
        Object[] args = getArguments(mostSpecificMethod, json, vo);
        return invoke(service, method, args);
    }


    /**
     * get the first parameter of the method, as the value object, and instance, set values from the json.
     * if the json is null or empty, means no value object, then return null.
     * if there is no parameters of the method, then return null.
     *
     * @param method
     * @param json
     * @return the value object with it's data.
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private Object getValueObj(Method method, Map<String, Object> json) {
        if (MapUtil.isEmpty(json)) return null;
        Class<?>[] allOfParameterTypes = method.getParameterTypes();
        if (allOfParameterTypes.length == 0) return null;
        Class<?> firstOfParameterType = allOfParameterTypes[0];
        if (Map.class.isAssignableFrom(firstOfParameterType)) return json;
        if (EntityUtils.isEntity(firstOfParameterType)) {
            return createEntityAndValidate(firstOfParameterType, json);
        }
        // 方法中泛型类型始终都是object，通过获取类上泛型类型来确定方法参数类型
        boolean flag = false;
        Class<?> clazz = null;
        final Class<?> superclass = method.getDeclaringClass().getSuperclass();
        if (Objects.isNull(superclass)) return null;
        final ParameterizedType parameterizedType = (ParameterizedType) superclass.getGenericSuperclass();
        if (Objects.isNull(parameterizedType)) return null;
        final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        for (Type actualTypeArgument : actualTypeArguments) {
            try {
                clazz = Class.forName(actualTypeArgument.getTypeName());
                if (flag = EntityUtils.isEntity(clazz)) break;
            } catch (ClassNotFoundException e) {
                // ignore
            }
        }
        if (flag && Object.class.isAssignableFrom(firstOfParameterType))
            return createEntityAndValidate(clazz, json);
        return null;
    }

    /**
     * 创建entity并且验证entity字段合法性
     *
     * @param clazz
     * @param json
     * @return
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private Entity createEntityAndValidate(Class<?> clazz, Map<String, Object> json) {
        final Entity entity = EntityUtils.createEntity((Class<Entity>) clazz, json);
        validate(entity);
        return entity;
    }

    /**
     * invoke the method of the service using reflect.
     * if the value object is available, it must be the first argument.
     *
     * @param service the service
     * @param method  the method
     * @param args    the other arguments
     * @return the result after invoking.
     */
    private Object invoke(Object service, Method method, Object[] args) {
        try {
            return ArrayUtil.isEmpty(args)
                    ? method.invoke(service)
                    : method.invoke(service, args);
        } catch (IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new JapiOrmException("error when invoking the service by reflect", e);
        }
    }
}
